package ge.tsu.java.project.server;

import ge.tsu.java.project.commons.BinaryProtocol;
import ge.tsu.java.project.commons.Contact;
import ge.tsu.java.project.commons.Logger;
import ge.tsu.java.project.commons.Message;
import ge.tsu.java.project.commons.Message.Type;
import ge.tsu.java.project.commons.Protocol;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server {

	private static Logger LOGGER = Logger.getInstance(Server.class);

	private ServerConfig config;
	private ServerSocket serverSocket;
	private AcceptThread acceptThread;
	
	private static ArrayList<Client> onlineClients = new ArrayList<>();

	private Server(ServerConfig config) {
		this.config = config;
	}

	public static Server getInstance() {
		return getInstance(ServerConfig.getDefault());
	}

	public static Server getInstance(ServerConfig config) {
		return new Server(config);
	}
	
	public synchronized static ArrayList<Client> getOnlineClients() {
		return new ArrayList<>(onlineClients);
	}
	
	public static synchronized void addClient(Client client) {
//		Contact contact = client.toContact(Status.ONLINE);
//		notifyStatus(contact);
		onlineClients.add(client);
	}
	
	public static synchronized void removeClient(Client client) {
		if (!onlineClients.remove(client)) {
			LOGGER.log("Seems client: " + client + " is offline");
			return;
		}
//		Contact contact = client.toContact(Status.OFFLINE);
//		notifyStatus(contact);
	}

	@SuppressWarnings("unused")
	private static void notifyStatus(Contact contact) {
		for (Client c : onlineClients) {
			Protocol prot = c.getProtocol();
				try {
					synchronized (prot) {
						prot.writeMessage(Message.newMessage(Type.CONTACT_STATUS_CHANGED));
						prot.writeSerializable(contact);
					}
				} catch (IOException e) {
					LOGGER.err("Could not notify status change to conneced clients", e);
					continue;
				}
		}
	}
	
	public static synchronized Client findClient(String id) {
		if (id == null) return null;
		for (Client c : onlineClients) {
			if (id.equals(c.getId())) {
				return c;
			}
		}
		return null;
	}

	public void start() {
		int port = config.getPort();
		try {
			serverSocket = new ServerSocket(port);
		} catch (IOException e) {
			LOGGER.severe("Unable to start server", e);
		}
		acceptThread = new AcceptThread(config.getPoolSize());
		acceptThread.setDaemon(false);
		acceptThread.start();
		LOGGER.log("Server started, listening at port: " + port);
	}

	public void stop() {
		if (serverSocket != null) {
			try {
				acceptThread.stopAccepting();
				serverSocket.close();
				serverSocket = null;
				LOGGER.log("Server stopped");
			} catch (IOException e) {
				LOGGER.err("Error while stopping server", e);
			}
		}
	}

	private class AcceptThread extends Thread {

		private ExecutorService clientTaskPool;
		private boolean accepting = true;

		public AcceptThread(int poolSize) {
			clientTaskPool = Executors.newFixedThreadPool(poolSize);
		}

		private void stopAccepting() {
			accepting = false;
			clientTaskPool.shutdownNow();
		}

		@Override
		public void run() {
			Socket client;
			while (accepting) {
				try {
					client = serverSocket.accept();
					LOGGER.log("Accepted new client: " + client);
				} catch (IOException e) {
					if (accepting) {
						Server.this.stop();
						LOGGER.severe("Error while listening for incoming connection", e);
					} // else server stopped
					return;
				}
				clientTaskPool.submit(new Task(client));
			}
		}

	}

	private static class Task implements Runnable {

		private Socket clientSocket;

		Task(Socket clientSock) {
			clientSocket = clientSock;
		}

		@Override
		public void run() {
			InputStream in = getInputStream();
			OutputStream out = getOutputStream();
			if (in == null || out == null) {
				return;
			}
			Protocol protocol = new BinaryProtocol(in, out);
			ClientProcessor processor = new ClientProcessor(protocol);
			try {
				processor.process();
			} catch (IOException e) {
				// if client died, remove here also
				Server.removeClient(processor.getClient());
				LOGGER.err("Error while processing client", e);
				try {
					// XXX needed?
					clientSocket.close();
				} catch (IOException closex) {
				}
			}
		}

		private OutputStream getOutputStream() {
			OutputStream out = null;
			try {
				out = clientSocket.getOutputStream();
			} catch (IOException e) {
				LOGGER.err("Could not get outputstream", e);
			}
			return out;
		}

		private InputStream getInputStream() {
			InputStream in = null;
			try {
				in = clientSocket.getInputStream();
			} catch (IOException e) {
				LOGGER.err("Could not get inputstream", e);
			}
			return in;
		}

	}

}
